[Exporter.Prometheus] Improve performance#7279
Conversation
- Add support for configuring OpenTelemetry.Exporter.Prometheus.HttpListener with the `OTEL_EXPORTER_PROMETHEUS_HOST` and `OTEL_EXPORTER_PROMETHEUS_PORT` environment variables. - Remove field for UriPrefixes and use auto-property. - Remove `UriPrefixes` from the README. Fixes open-telemetry#4158. Fixes open-telemetry#7154.
Fix markdownlint warnings.
Add coverage for invalid environment variables.
Remove extra semicolons.
React to changes from open-telemetry#7175.
Remove duplicated constructor declaration.
- Add missing SHOULD requirement to specify the `escaping` value for 1.0.0 protocols. - Update `PrometheusSerializer` to be compliant with `escaping=underscores` when using OpenMetrics.
Fix missing prefixing for metrics that start with a digit.
- Use canonical representations for numbers for "le" label values of histograms and "quantile" label values of summary metrics for OpenMetrics. - Resolve TODO by moving check outside loop. See https://prometheus.io/docs/specs/om/open_metrics_spec/#considerations-canonical-numbers.
Add CHANGELOG entries.
Remove branches that could not be reached.
- Check destination size. - Update CHANGELOGs.
Fix incorrect serialized value for `PrometheusType.Untyped` when using OpenMetrics.
Omit histogram `_sum` and `_count` in OpenMetrics when negative bucket thresholds are present.
Update PR number.
Export `{name}_created` series for counters and histograms when start time is available when using OpenMetrics.
Add missing `TYPE` metadata.
- Remove non-spec `TYPE` for `_counter`. - Fix-up timestamp precision.
Fix-up duplicated definitions.
Fix-up merge.
Add missing suffix.
- Emit OpenMetrics scope metadata as a single `otel_scope` metric family with `otel_scope_info` samples instead of repeating metadata for every scope. - Include instrumentation scope metadata on samples using `otel_scope_*` labels, including scope version, schema URL, and prefixed scope attributes. - Drop conflicting scope attributes named `name`, `version`, and `schema_url` to avoid collisions with generated scope labels.
Remove Go theme for .NET.
Add CHANGELOG entries.
Address Copilot review feedback.
Add more test coverage for patch.
Add Prometheus text fallback `target_info` output as a gauge so resource metadata is still exposed as Info-typed metrics are unavailable for PrometheusText exposition format.
Add PR number.
Merge colliding sanitized label keys by concatenating values in lexicographic order of the original keys.
Revert some changes from merge with main which aren't needed.
Restore optimisations that were lost in merge.
There was a problem hiding this comment.
Pull request overview
This PR targets performance improvements in the Prometheus HttpListener exporter serialization path by reducing per-metric string conversions, optimizing escaping and numeric formatting on newer TFMs, and reusing pre-serialized data (metric names and tag sets) across repeated writes (notably histogram bucket output).
Changes:
- Add faster escaping implementations (using
SearchValues<char>on .NET 8+) and write common label value types directly to UTF-8. - Cache pre-serialized metric name / metadata name / unit bytes on
PrometheusMetricand write them as UTF-8 bytes instead of char-by-char loops. - Pre-serialize histogram tag sets once per
MetricPointand reuse across bucket/sum/count output; broaden buffer-growth retry logic to includeArgumentExceptionin addition toIndexOutOfRangeException.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs | Reuse pre-serialized tag bytes for histogram output and add helpers for writing serialized tag spans. |
| src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs | Optimize escaping and label value formatting; write cached metric/unit bytes directly to the output buffer. |
| src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusMetric.cs | Cache ASCII byte representations of metric-related names/units; adjust OpenMetrics naming/unit handling. |
| src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs | Treat ArgumentException as a buffer-growth signal during serialization retries (like IndexOutOfRangeException). |
Comments suppressed due to low confidence (3)
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs:268
- This
catch (Exception ex) when (ex is IndexOutOfRangeException or ArgumentException)introduces an unusedexvariable and is less explicit than catching the two expected exception types directly. CatchingIndexOutOfRangeExceptionandArgumentExceptionseparately keeps the intent clear and avoids the unused variable.
catch (Exception ex) when (ex is IndexOutOfRangeException or ArgumentException)
{
if (!IncreaseBufferSize(ref buffer))
{
throw;
}
}
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs:285
- This
catch (Exception ex) when (ex is IndexOutOfRangeException or ArgumentException)introduces an unusedexvariable and is less explicit than catching the two expected exception types directly. CatchingIndexOutOfRangeExceptionandArgumentExceptionseparately keeps the intent clear and avoids the unused variable.
catch (Exception ex) when (ex is IndexOutOfRangeException or ArgumentException)
{
if (!IncreaseBufferSize(ref buffer))
{
throw;
}
}
src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusCollectionManager.cs:337
- This
catch (Exception ex) when (ex is IndexOutOfRangeException or ArgumentException)introduces an unusedexvariable and is less explicit than catching the two expected exception types directly. CatchingIndexOutOfRangeExceptionandArgumentExceptionseparately keeps the intent clear and avoids the unused variable.
catch (Exception ex) when (ex is IndexOutOfRangeException or ArgumentException)
{
if (!IncreaseBufferSize(ref buffer))
{
throw;
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Revert change from merge that is now redundant (and wrong).
|
Copilot summary of comparing the results from running
Largest improvements were in |
Shorten to just NET.
Kielek
left a comment
There was a problem hiding this comment.
One NIT + some codex feedback
[P2] src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializer.cs:257: the new typed label-value formatter is only used on the fast path. When WriteTags falls back for sanitized key collisions, AddLabel still stores values through GetLabelValueString at line 956, which uses current-culture ToString() for everything except bool/double/float at line 570. That makes the same label value serialize differently depending on whether a collision occurs. For example, under fr-FR, a non-colliding decimal point tag now emits 1.23, but colliding tags that go through merge can emit 1,23;.... The fallback path should use the same invariant formatting logic as WriteLabelValue(object?).
- Add comment. - Fix slow-path to also use invariant formatting. - Simplify some tests.
Changes
Improve the performance of
PrometheusSerializerby:SearchValues<T>on .NET 8+ToString()See #7279 (comment) for results.
Supersedes #7170.
Merge requirement checklist
AppropriateCHANGELOG.mdfiles updated for non-trivial changesChanges in public API reviewed (if applicable)